using MongoDB.Driver;
using SimpleInjector;
using Sustainalytics.BusinessResearchProductStates.Entities.ResearchState;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.ClientDW.Entities.Universes;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ConnectionStrings;
using Sustainalytics.Entities.Governance;
using Sustainalytics.Entities.ProductInvolvement;
using Sustainalytics.Entities.RiskRatings;
using Sustainalytics.Entities.ScreeningTool;
using Sustainalytics.Entities.SustainableProducts;
using Sustainalytics.ESG.Entities;
using Sustainalytics.MongoDB.Extensions;
using Sustainalytics.ScreeningTool.Synchronization.Caches;
using Sustainalytics.ScreeningTool.Synchronization.Entities;
using Sustainalytics.ScreeningTool.Synchronization.Handlers;
using Sustainalytics.ScreeningTool.Synchronization.Logging;
using Sustainalytics.ScreeningTool.Synchronization.Processors;
using System;
using System.Collections.Generic;
using System.Configuration.Abstractions;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using EventIndicator = Sustainalytics.ClientDW.Entities.EventIndicator;
using Incident = Sustainalytics.ClientDW.Entities.Incident;
using RiskRatingEventIndicator = Sustainalytics.Entities.RiskRatings.EventIndicator;

namespace Sustainalytics.ScreeningTool.Synchronization
{
    public static class SimpleInjectorConfig
    {
        public static Container Register(
            ClientFacingProduct clientFacingProduct,
            Dictionary<string, string> connectionStrings,
            IConfigurationManager configurationManager)
        {
            var assemblies = new[] { typeof(ScreeningDataCreator).Assembly };

            var container = new Container();

            container.RegisterLoggers();
            container.RegisterCollections(connectionStrings);
            container.RegisterCaches(assemblies);
            container.RegisterHandlersAndProcessors(assemblies);
            container.RegisterMisc(clientFacingProduct, configurationManager);

            container.Verify();

            return container;
        }

        public static async Task InitializeCaches(this Container container, ClientFacingProduct clientFacingProduct)
        {
            var caches = container.GetAllInstances<CacheBase>();
            await caches.ParallelForEachAsync(cache =>
            {
                var loaderType = typeof(ICacheLoader<>).MakeGenericType(cache.GetType());
                var loader = (ICacheLoader)container.GetInstance(loaderType);
                return loader.LoadAsync(clientFacingProduct);
            }, Environment.ProcessorCount / 2);
        }

        private static void RegisterMisc(this Container container, ClientFacingProduct clientFacingProduct, IConfigurationManager configurationManager)
        {
            container.RegisterInitializer<IHasClientFacingProduct>(x => x.ClientFacingProduct = clientFacingProduct);
            container.Register(() => configurationManager.AppSettings);
            container.Register(() => container.GetInstance<IMongoCollection<GCCurrentQuarter>>().AsQueryable().Single());
            container.Register<ScreeningDataCreator>();
        }

        private static void RegisterLoggers(this Container container)
        {
            container.RegisterDecorator(typeof(ICacheLoader<>), typeof(CacheLoaderDecorator<>));
            container.RegisterDecorator<IProcessor, ProcessorLoggingDecorator>();
            container.RegisterDecorator(typeof(IHandler<>), typeof(HandlerLoggingDecorator<>));
            container.RegisterDecorator(typeof(IHandlerWithGrouping<>), typeof(HandlerWithGroupingLoggingDecorator<>));
        }

        private static void RegisterCollections(this Container container, Dictionary<string, string> connectionStrings)
        {
            container.RegisterClientDWCollections(connectionStrings[ConnectionStringType.ClientDW.ToString()]);
            container.RegisterClientPlatformCollections(connectionStrings[ConnectionStringType.ClientPlatform.ToString()]);
            container.RegisterGovernanceDWCollections(connectionStrings[ConnectionStringType.GovernanceDW.ToString()]);
            container.RegisterScreeningDWCollections(connectionStrings[ConnectionStringType.ScreeningDW.ToString()]);
            container.RegisterClientUniversesCollections(connectionStrings[ConnectionStringType.ClientUniverses.ToString()]);
            container.RegisterRiskRatingsCollections(connectionStrings[ConnectionStringType.RiskRatingsResearchDW.ToString()]);
            container.RegisterProductInvolvementCollections(connectionStrings[ConnectionStringType.ProductInvolvementDW.ToString()]);
        }

        private static void RegisterClientDWCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CompanyKeyESGIssue>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CorporateData>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<EsgResearchProfile>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CompanyProfileResearchState>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<EventIndicator>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<FinancialData>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GCCurrentQuarter>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GCOverallCompanySummaryExtended>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GCPrincipleSummaryExtended>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<Incident>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<IndicatorTemplate>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RPCompanyESGComputationResult>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RPControversyIndicator>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CarbonData>(connectionString));
        }

        private static void RegisterClientPlatformCollections(this Container container, string connectionString)
        {
            var collectionName = MongoFactory.CollectionExistsInDatabase("FieldDefinition", connectionString)
                ? "FieldDefinition"
                : "TemplateCriteria";
            container.Register(() => MongoFactory.GetCollectionFromDatabase<FieldDefinition>(connectionString, collectionName));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<Account>(connectionString));
        }

        private static void RegisterGovernanceDWCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GovernanceCompanyProfile>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GovernanceComputationResult>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GovernanceIssue>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GovernanceIndicator>(connectionString));
        }

        private static void RegisterScreeningDWCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<ScreeningData>(connectionString));
        }

        private static void RegisterClientUniversesCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<Universe>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<UniverseCompany>(connectionString));
        }

        private static void RegisterRiskRatingsCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RiskRatingBenchmark>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RiskRatingResearchProfile>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CoreRiskRatingResearchProfile>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RiskRatingMaterialEsgIssue>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<ComprehensiveRiskRatingIndicator>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<CoreRiskRatingIndicator>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<RiskRatingEventIndicator>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<GovernancePillars>(connectionString));
        }

        private static void RegisterProductInvolvementCollections(this Container container, string connectionString)
        {
            container.Register(() => MongoFactory.GetCollectionFromDatabase<ProductInvolvementData>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<ProductInvolvementFramework>(connectionString));
            container.Register(() => MongoFactory.GetCollectionFromDatabase<SustainableProductsData>(connectionString));
        }

        private static void RegisterCaches(this Container container, Assembly[] assemblies)
        {
            var cacheTypes = container.GetTypesToRegister(typeof(CacheBase), assemblies);
            container.RegisterCollection<CacheBase>(cacheTypes);
            foreach (var cacheType in cacheTypes)
            {
                container.Register(cacheType, () => Activator.CreateInstance(cacheType), Lifestyle.Singleton);
            }
            container.Register(typeof(ICacheLoader<>), assemblies);
        }

        private static void RegisterHandlersAndProcessors(this Container container, Assembly[] assemblies)
        {
            var processorTypesToRegister = HandlerProcessorLinks.SelectMany(link =>
            {
                var typesImplementingIHandler = container.GetTypesToRegister(link.HandlerType, assemblies);
                var typesDerivedFromProcessor = container.GetTypesToRegister(link.ProcessorType, assemblies);
                var genericArgumentsOfTypesDerivedFromProcessor = typesDerivedFromProcessor
                    .SelectMany(t => t.BaseType.GetGenericArguments());

                container.Register(link.HandlerType, typesImplementingIHandler);

                var typesImplementingIProcessor = typesImplementingIHandler
                    .SelectMany(type => type.GetInterfaces().Where(x => x.Name == link.HandlerType.Name)).Distinct()
                    .Where(i => !i.GetGenericArguments().Any(t => genericArgumentsOfTypesDerivedFromProcessor.Contains(t)))
                    .Select(i => link.ProcessorType.MakeGenericType(i.GetGenericArguments()))
                    .Concat(typesDerivedFromProcessor);

                return typesImplementingIProcessor;
            });

            container.RegisterCollection<IProcessor>(processorTypesToRegister);
        }

        private static readonly IEnumerable<HandlerProcessorLink> HandlerProcessorLinks = new[]
        {
            new HandlerProcessorLink(typeof(IHandler<>), typeof(Processor<>)),
            new HandlerProcessorLink(typeof(IHandlerWithGrouping<>), typeof(ProcessorWithGrouping<>)),
        };

        private class HandlerProcessorLink
        {
            public HandlerProcessorLink(Type handlerType, Type processorType)
            {
                HandlerType = handlerType;
                ProcessorType = processorType;
            }

            public Type HandlerType { get; }
            public Type ProcessorType { get; }
        }
    }
}
